package com.sky.service.impl;

import com.sky.dto.GoodsSalesDTO;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.ReportService;
import com.sky.service.WorkspaceService;
import com.sky.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author DragonflyYue
 */
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private WorkspaceService workspaceService;
    /**
     * 统计指定时间区间内的营业额数据(比如从12.1号开始,近7天的营业额)
     * @param begin
     * @param end 
     * @return
     */
    @Override
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {

        //当前集合用于存放从begin日期到end日期 范围内的每天的日期
        List<LocalDate> dateList = new ArrayList();
        
        dateList.add(begin);   //第一个日期肯定是  begin
        while (!(begin.equals(end))){
            //日期计算,,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);   //表示每次都加1天,然后赋值给bedin,一直到临街条件,begin = end
            dateList.add(begin);   //相当于这里把第2天加入到集合中,,后面还会有第3天,第4天,一直到第end天
        }
        
       //最终我们需要封装到 TurnoverReportVO,所以上面的List集合还要封装成这个VO类型
        
        //将上面的List集合转换成String形式,因为TurnoverReportVO里面dateList是String类型
        String dateListSr = StringUtils.join(dateList, ",");
        //1.需要注意这个StringUtils是long3包下的,别导入错了,要不找不到对应的join方法;基础笔记还真没学过
            //2. 上面的separator="," 表示用逗号进行分割,即把dateList中的每个元素取出来,然后通过逗号分割

        //用来存放每一天的营业总额     turnover就是营业额的意思
        List<Double> turnoverList = new ArrayList();
        
        for (LocalDate date : dateList) { 
            //把集合中每个元素取出来
            //查询date日期对应的营业额数据,营业额: 状态为"已完成"的订单金额合计
                //select sum(amount) from orders where status = 5 and  order_time > ? and order_time < ?
                //其中: amount指的是金额总数; sum是sql语句中的求和函数
            
            //获取到对应的详细时间,,  因为上面的date只是获取到日期,我们要精确到这天的开始和结束时间
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
                //表示获取到date这天的开始的时刻,就是0点0分0秒
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);  //每天的结束时间

            //所以详细的sql语句就应该是:
                //select sum(amount) from orders where status = 5 and order_time > beginTime and order_time < endTime
            
            //看上面的sql语句,我们也能知道,,要传入三个参数,因此下面封装到一个map中,直接传入map就可以了
            Map map = new HashMap();
            map.put("begin",beginTime);
            map.put("end",endTime);
            map.put("status", Orders.COMPLETED);   //这个常量就是数字5,表示订单已完成的状态
            
            //查询数据库,直接传入参数map
            Double turnover = orderMapper.sumByMap(map);   //turnover就是营业额的意思,当天已完成订单总额的求和

            //判断一下,,如果当天的营业额为null,就给他转成一个0.0,如果不是null,就还用它自己的值turnover
                //原因:  因为不判断,下面会把null添加到turnoverList集合中了,最终返回给商家统计图是个null,小瑕疵
            turnover =  turnover == null ? 0.0 : turnover;   
                //后面部分是一个三目运算符,最终的结果 会赋值给turnover
            
            //把当天查询到的营业总额,放入到上面的turnoverList,,这个List集合中
            turnoverList.add(turnover);
        }
        //把获得到的List<Double> turnoverList转换成String类型,
            // 因为TurnoverReportVO里面的turnoverList就是String类型
        String turnoverListSr = StringUtils.join(turnoverList, ",");

        TurnoverReportVO turnoverReportVO = TurnoverReportVO.builder()
                .dateList(dateListSr)
                .turnoverList(turnoverListSr)
                .build();
        
        return turnoverReportVO;
    }

    /**
     * 统计指定时间区间内的用户数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        //存放我们从begin到end之间对应的日期
        List<LocalDate> dateList = new ArrayList<>();
        
        dateList.add(begin);
        
        while (!(begin.equals(end))){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        //存放每天新增的用户数量
        List<Integer>  newUserList = new ArrayList<>();
        //存放每天的总用户数量
        List<Integer>  totalUserList = new ArrayList<>();   
        
        //查询用户用的是userMapper,我还以为是查订单表呢
            //我们怎么判断这个用户是不是新增的呢?原来User类中有一个createTime注册时间
        //select count(id) from user where create_time > ? and create_time < ?
        //select count(id) from user where  create_time < ?

        for (LocalDate date : dateList) {
            //遍历出每一天,然后获取到每天的用户数量
            
            //因为要精确到时分秒,所以要对这个date再处理下
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); //当天的开始时间
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); //当天的结束始时间
            
            //因为下面mapper中sql语句,我们定义的方法传入的参数是map类型,所以要封装一个map
            Map map = new HashMap();
            map.put("end",endTime);
            
            //总用户数量
            Integer totalUser = userMapper.countByMap(map);   
                //mapper中对应sql语句是动态拼接的,这里begin=null,那就不会拼接对应的条件,所以查的就是总的用户数量
            
            map.put("begin",beginTime);
            
            //新增用户的数量---这里不理解就去看mapper中的sql语句,是根据当天的begin和end时间,来查询当天的新用户的
            Integer newUser = userMapper.countByMap(map);
                //这也是为什么上面要先加入end条件的原因,可以先查出来总的用户数量;
                // 不这样写,就得单独写两个sql语句,就不是动态条件的;这个条件少,感觉不写动态的sql完全可以
            
            //封装到集合中
            totalUserList.add(totalUser);
            newUserList.add(newUser);
        }
        
        //最后把所有的属性封装成VO对象返回
            //不过上面的List集合,还应该处理成String格式,我懒得单独处理了,传入参数直接通过StringUtils.join处理的
        UserReportVO userReportVO = UserReportVO.builder()
                .dateList(StringUtils.join(dateList,","))
                .newUserList(StringUtils.join(newUserList,","))
                .totalUserList(StringUtils.join(totalUserList,","))
                .build();

        return userReportVO;
    }

    /**
     * 统计指定时间区间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end) {
        //1.存放我们从begin到end之间对应的日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);

        while (!(begin.equals(end))){
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        
        //2.new两个集合,存放下面遍历得到的orderCountList和validOrderCountList
        //存放每天的订单总数
        List<Integer> orderCountList = new ArrayList<>();
        //存放每天的有效订单总数
        List<Integer> validOrderCountList = new ArrayList<>();
        
        //2.1遍历dateList集合,查询每天的有效订单数和 订单总数
        for (LocalDate date : dateList) {
            //查询每天的订单总数    select count(id) from orders where order_time < ? and order_time > ?
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); //当天的开始时间
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);  //当天的结束时间
            
                //因为查询每天订单总数,,这里不需要传入status的条件,,所以直接传入null就可以了;动态sql语句就不会拼接这个条件
            Integer orderCount = getOrderCount(beginTime, endTime, null);

            //查询每天的有效订单数   下面的sql语句,status=5,表示订单的状态是已完成.
                // select count(id) from orders where order_time < ? and order_time > ? and status = 5
            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);  //后面的是常量,值为5
            
            //把查出来的数据,存放到集合中
            orderCountList.add(orderCount);
            validOrderCountList.add(validOrderCount);
        }
        
        //3.1计算时间区间内的订单总数量
            //老师说,这里可以用stream流,可以把集合中的元素累加到一起,,,比我们传统的遍历更简洁(我一点也没感觉简洁,,尝试着用)
            //1. reduce就是一个合并方法,(Integer::sum)这个就表示Interger类型的求和 ,会把集合中的元素都遍历一遍
            //2. 最后的get方法,表示获取对应的求和过后的值
        Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();

        //3.2计算时间区间内的有效订单总数量
        Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();

        //3.3 计算订单完成率
        Double orderCompletionRate = 0.0;   
        if (totalOrderCount != 0){   //这个除数不能为0,所以要单独判断下;如果除数为0,就不执行下面代码逻辑,而上面有初始化结果为0.0了
            // 计算订单完成率
                //需要注意,下面的doubleValue()就是把Integer类型转换成double类型,然后double/Integer,最终还是double类型,基础讲过
                //其实也可以直接用: Double.valueOf把最终的结果转换成double类型; 但上面的方法更精确,以前都讲过
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
            
        }
        
        //4. VO的六个属性都准备好了,,最终封装好VO对象
        OrderReportVO orderReportVO = OrderReportVO.builder()
                .dateList(StringUtils.join(dateList,","))
                .orderCountList(StringUtils.join(orderCountList,","))
                .validOrderCountList(StringUtils.join(validOrderCountList,","))
                .totalOrderCount(totalOrderCount)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
        
        return orderReportVO;
        
        //5. 等下一个项目,熟悉一点儿了,就直接return返回,上面就不单独声明用OrderReportVO orderReportVO接收,,直接return反悔了
            //而这里,主要是便于将来自己回顾代码,更好理解
    }
    
    /**
     * 根据条件统计订单数量
     * 相当于单独把方法抽出来了,,然后上面方法用到,就可以直接调用这个方法,,,特别是对于复用多的代码都可以单独提出来
     * @param begin
     * @param end
     * @param status
     * @return
     */
    private Integer getOrderCount(LocalDateTime begin,LocalDateTime end,Integer status){
        Map map = new HashMap();
        map.put("begin",begin);
        map.put("end",end);
        map.put("status",status);

        Integer orderCount = orderMapper.countByMap(map);
        
        return orderCount;
        
    }

    /**
     * 统计指定时间区间内的销量排名前10
     * @param begin
     * @param end
     * @return
     */
    @Override
    public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
        //1.处理日期格式
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
        //2. 数据库查询出对应的数据
        List<GoodsSalesDTO> saleTop10 = orderMapper.getSaleTop10(beginTime, endTime);
        
        //3.接下来对返回的结果,封装成SalesTop10ReportVO
            //正常自己的想法,就是直接遍历,然后单独取出对应的name和number,最后再拼接成对应的集合,最终处理成VO的要求,进行封装
        
        //但是上面自己遍历,然后自己处理逻辑没问题,,但是处理起来肯定很繁琐
        //3.1 所以我们要用stream流来实现
        List<String> names = saleTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
            //代码解析:  
            //1.saleTop10.stream()拿到stream对象
            //2. .map(GoodsSalesDTO::getName) 就能通过getName拿出每个name的值,这里相当于是把整个List的name都拿出来了
            //3. .collect(Collectors.toList())  最终把拿出来的所有结果,封装成一个集合,一个List集合
        
        //3.2 再通过StringUtils将集合转成string字符串格式,通过","分割
        String nameList = StringUtils.join(names, ",");

        //3.3 在处理numberList
        List<Integer> numbers = saleTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
        String numberList = StringUtils.join(numbers, ",");
        
        //4.最终封装VO
        SalesTop10ReportVO salesTop10ReportVO = SalesTop10ReportVO.builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
        
        return salesTop10ReportVO;
    }

    /**
     * 导出运营数据报表
     * @param response
     */
    @Override
    public void exportBusinessDate(HttpServletResponse response) {

        //1. 查询数据库,获取营业数据------查询最近30天的运营数据
            //其实前面在工作台就完成了查询相应的功能,可以对照excel模版来看需要查询哪些数据

        LocalDate dateBegin = LocalDate.now().minusDays(30);//相对于今天来说,往前面数30天的日期
        LocalDate dateEnd = LocalDate.now().minusDays(1);   //这个是昨天的日期,,,查询最近30天,肯定不能包括今天,今天订单可能还没完成
        //对日期格式进行处理(因为上面只精确到天,没有时分秒)
        LocalDateTime begin = LocalDateTime.of(dateBegin, LocalTime.MIN);  //后面的常量是: 0点0分0秒,即当天的开始
        LocalDateTime end = LocalDateTime.of(dateEnd, LocalTime.MAX);
        //查询对应的营业数据
        BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end);

        //2. 通过POI将数据写入到Excel文件中
            //.getClass()获取类对象;  .getClassLoader获取类加载器  .getResourceAsStream()从类路径下面读取资源
            //当前我们的资源,就是excel模版,就在这个目录下,resources\template\运营数据报表模板.xlsx   
        //获取输入流对象,后面有对应的文件(自己理解,就是读取到内存了)
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");

        try {
            //基于模版文件,创建一个新的excel (自己理解:  反正也都是在内存中)
            XSSFWorkbook excel = new XSSFWorkbook(inputStream);
            //获取表格文件sheet标签页,,这里是通过名称获取的
            XSSFSheet sheet = excel.getSheet("Sheet1");//这里是根据sheet页的名字获取的,,前面讲的是根据下角标获取的
            //填充数据,把我们查询的营业数据,填充到这个excel文件中(需要对照模版文件来看,都需要填充哪些数据)
            //填充-时间
                //因为行已经存在,,所以我们直接获取行,而不用创建行了
                //获取第2行的第2个单元格,并对其赋值,,后面具体的值,就进行了拼接,就能填充起始日期
            sheet.getRow(1).getCell(1).setCellValue("时间: " + dateBegin + "至" + dateEnd);

            //填充-营业额
            //获得第4行
            XSSFRow row = sheet.getRow(3);
            //获取第3个单元格,并填充对应的营业额,,Turnover就是营业额
            row.getCell(2).setCellValue(businessDataVO.getTurnover());
            //填充-订单完成率
            row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
            //填充-新增用户数
            row.getCell(6).setCellValue(businessDataVO.getNewUsers());

            //获得第5行
            row = sheet.getRow(4);   //这里没有创建新的对象,那说明上面的row数值就失效了,当前是第5行
            //填充-有效订单
            row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
            //填充-平均客单价
            row.getCell(4).setCellValue(businessDataVO.getUnitPrice());

            //填充明细数据--就是每一天的详细数据,总共30天的
            for (int i = 0; i < 30; i++) {
                //这样就能实现日期每次往后面走1天;不断遍历,就能把每一天的数据填充上
                LocalDate date = dateBegin.plusDays(i);  
                //查询某一天的营业数据(后面传入的参数,是当前的起始时间和结束时间,,以前都讲过,,忘了可以翻翻前面笔记)
                BusinessDataVO businessData = workspaceService.getBusinessData(
                        LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));

                //获取某一行
                //看excel模版文件,要从第8行开始填充;而我们为了和遍历条件关联起来;所以下面+i;学过c++这都很好理解的
                row = sheet.getRow(7 + i);  //当前第1次,获取到的是第8行
                //获取对应的单元格,并填充对应的内容
                    //这里是这行的第2个单元格,要填充的内容是"日期"
                row.getCell(1).setCellValue(date.toString());  //这个方法接收的是String类型,所以用toString转一下
                //填充下一个单元格,填充"营业额"
                row.getCell(2).setCellValue(businessData.getTurnover());
                //填充下一个单元格,,"有效订单"
                row.getCell(3).setCellValue(businessData.getValidOrderCount());
                //填充下一个单元格,"订单完成率"
                row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
                //填充下一个单元格,"平均客单价"
                row.getCell(5).setCellValue(businessData.getUnitPrice());
                //填充下一个单元格,"新增用户数"
                row.getCell(6).setCellValue(businessData.getNewUsers());

            }

            //3. 通过输出流将excel文件,下载到客户端浏览器(为啥不叫上传,上传一般是上传到服务器)
            //获取输出流对象,要把excel文件传给客户端浏览器
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);   //这里就能把这个excel写回到客户端浏览器

            //4.关闭资源
            out.flush();
            out.close();
            excel.close();


        } catch (IOException e) {
            e.printStackTrace();
        }
    }


}
